﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows.Markup;
using SlimDX.Direct3D10;
using System.Diagnostics.CodeAnalysis;
using SlimDX.DXGI;
using System.Diagnostics;
using System.ComponentModel;
using SlimDX;

namespace System.Windows.Media.Media3D10
{
    /// <summary>
    /// Représente une scène 3-D
    /// </summary>
    [ContentProperty("Children")]
    public class Viewport3D10 : FrameworkElement
    {
        #region Dependency Properties
        /// <summary>
        /// Identifie la propriété de dépendance <see cref="Camera"/>.
        /// </summary>
        public static readonly DependencyProperty CameraProperty =
            DependencyProperty.Register("Camera", typeof(Camera), typeof(Viewport3D10),
            new PropertyMetadata(new PropertyChangedCallback(Camera_PropertyChanged)));

        /// <summary>
        /// Identifie la propriété de dépendance <see cref="Light"/>.
        /// </summary>
        public static readonly DependencyProperty LightProperty =
            DependencyProperty.Register("Light", typeof(Light), typeof(Viewport3D10),
            new PropertyMetadata(new PropertyChangedCallback(Light_PropertyChanged)));

        /// <summary>
        /// Identifie la propriété de dépendance <see cref="Skybox"/>.
        /// </summary>
        public static readonly DependencyProperty SkyboxProperty =
            DependencyProperty.Register("Skybox", typeof(SkyboxModel3D10), typeof(Viewport3D10),
            new PropertyMetadata(new PropertyChangedCallback(Skybox_PropertyChanged)));

        /// <summary>
        /// Identifie la propriété de dépendance <see cref="Children"/>.
        /// </summary>
        public static readonly DependencyProperty ChildrenProperty =
            DependencyProperty.Register("Children", typeof(Visual3D10Collection), typeof(Viewport3D10),
            new PropertyMetadata(new PropertyChangedCallback(Children_PropertyChanged)));
        #endregion

        #region Dependency Properties Callbacks
        private static void Camera_PropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(sender, null))
                throw new ArgumentNullException("sender");
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");

            (sender as Viewport3D10).OnCameraChanged(e);
        }

        private static void Light_PropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(sender, null))
                throw new ArgumentNullException("sender");
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");

            (sender as Viewport3D10).OnLightChanged(e);
        }

        private static void Skybox_PropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(sender, null))
                throw new ArgumentNullException("sender");
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");

            (sender as Viewport3D10).OnSkyboxChanged(e);
        }

        private static void Children_PropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(sender, null))
                throw new ArgumentNullException("sender");
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");

            (sender as Viewport3D10).OnChildrenChanged(e);
        }
        #endregion

        #region Constructors
        /// <summary>
        /// Initialise une nouvelle instance de la classe <see cref="Viewport3D10"/>
        /// </summary>
        public Viewport3D10()
            : base()
        {
            Children = new Visual3D10Collection();

            _d3d10Image = new D3D10Image();
            _d3d10Image.IsFrontBufferAvailableChanged += OnIsFrontBufferAvailableChanged;

            _timer = new Stopwatch();

            BeginRenderingScene();
        }
        #endregion

        #region Data
        private D3D10Image _d3d10Image;

        private static readonly Color4 _background = new Color4(1.0f, 0.0f, 0.0f, 1.0f);
        private Viewport _viewport;
        private Rect _viewportRect;

        private RenderTargetView _renderTargetView;
        private DepthStencilView _depthStencilView;
        
        private Texture2D _sharedTexture;
        private Texture2D _depthTexture;

        private Stopwatch _timer;
        #endregion

        #region Public Members
        /// <summary>
        /// Camera à utiliser pour le rendu
        /// </summary>
        public Camera Camera
        {
            get { return (Camera)GetValue(CameraProperty); }
            set { SetValue(CameraProperty, value); }
        }

        /// <summary>
        /// Lumière utilisée pour le rendu
        /// </summary>
        public Light Light
        {
            get { return (Light)GetValue(LightProperty); }
            set { SetValue(LightProperty, value); }
        }

        /// <summary>
        /// Texture d'environnement de la scène 3-D
        /// </summary>
        public SkyboxModel3D10 Skybox
        {
            get { return (SkyboxModel3D10)GetValue(SkyboxProperty); }
            set { SetValue(SkyboxProperty, value); }
        }

        /// <summary>
        /// Liste des <see cref="Visual3D10"/> de la scène 3-D
        /// </summary>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227")]
        public Visual3D10Collection Children
        {
            get { return (Visual3D10Collection)GetValue(ChildrenProperty); }
            set { SetValue(ChildrenProperty, value); }
        }
        #endregion

        #region Protected Members
        /// <summary>
        /// Appelé lors du changement de la camera
        /// </summary>
        /// <param name="e"><see cref="DependencyPropertyChangedEventArgs"/> contenant les informations sur le changement.</param>
        protected virtual void OnCameraChanged(DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");
        }

        /// <summary>
        /// Appelé lors du changement de la lumière
        /// </summary>
        /// <param name="e"><see cref="DependencyPropertyChangedEventArgs"/> contenant les informations sur le changement.</param>
        protected virtual void OnLightChanged(DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");
        }

        /// <summary>
        /// Appelé lors du changement de texture d'environnement
        /// </summary>
        /// <param name="e"><see cref="DependencyPropertyChangedEventArgs"/> contenant les informations sur le changement.</param>
        protected virtual void OnSkyboxChanged(DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");
        }

        /// <summary>
        /// Appelé lors du changement de la liste des enfants
        /// </summary>
        /// <param name="e"><see cref="DependencyPropertyChangedEventArgs"/> contenant les informations sur le changement.</param>
        protected virtual void OnChildrenChanged(DependencyPropertyChangedEventArgs e)
        {
            if (object.ReferenceEquals(e, null))
                throw new ArgumentNullException("e");
        }
        #endregion

        #region Private Members
        private void CompositionTarget_Rendering(object sender, EventArgs e)
        {
            _d3d10Image.InvalidateD3DImage();
            this.InvalidateVisual();
        }

        private void BeginRenderingScene()
        {
            if (_d3d10Image.IsFrontBufferAvailable)
            {
                _d3d10Image.SetBackBufferSlimDX(_sharedTexture);
                CompositionTarget.Rendering += CompositionTarget_Rendering;
                _timer.Start();
            }
        }

        private void StopRenderingScene()
        {
            _timer.Stop();
            CompositionTarget.Rendering -= CompositionTarget_Rendering;
        }

        private void OnIsFrontBufferAvailableChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            // This fires when the screensaver kicks in, the machine goes into sleep or hibernate
            // and any other catastrophic losses of the d3d device from WPF's point of view
            if (_d3d10Image.IsFrontBufferAvailable)
            {
                BeginRenderingScene();
            }
            else
            {
                StopRenderingScene();
            }
        }

        [SuppressMessage("Microsoft.Design", "CA1021")]
        private static RenderTargetView CreateRenderTargetView(int width, int height, out Texture2D texture)
        {
            Texture2DDescription colordesc = new Texture2DDescription();
            colordesc.BindFlags = BindFlags.RenderTarget | BindFlags.ShaderResource;
            colordesc.Format = Format.B8G8R8A8_UNorm;
            colordesc.Width = width;
            colordesc.Height = height;
            colordesc.MipLevels = 1;
            colordesc.SampleDescription = new SampleDescription(1, 0);
            colordesc.Usage = ResourceUsage.Default;
            colordesc.OptionFlags = ResourceOptionFlags.Shared;
            colordesc.CpuAccessFlags = CpuAccessFlags.None;
            colordesc.ArraySize = 1;

            texture = new Texture2D(Engine.Device10, colordesc);
            return new RenderTargetView(Engine.Device10, texture);
        }

        [SuppressMessage("Microsoft.Design", "CA1021")]
        private static DepthStencilView CreateDepthStencilView(int width, int height, out Texture2D depthTexture)
        {
            Texture2DDescription depthdesc = new Texture2DDescription();
            depthdesc.Width = width;
            depthdesc.Height = height;
            depthdesc.MipLevels = 1;
            depthdesc.ArraySize = 1;
            depthdesc.Format = Format.D32_Float_S8X24_UInt;
            depthdesc.SampleDescription = new SampleDescription(1, 0);
            depthdesc.Usage = ResourceUsage.Default;
            depthdesc.BindFlags = BindFlags.DepthStencil;
            depthdesc.OptionFlags = ResourceOptionFlags.None;
            depthdesc.CpuAccessFlags = CpuAccessFlags.None;

            depthTexture = new Texture2D(Engine.Device10, depthdesc);

            return new DepthStencilView(Engine.Device10, depthTexture);
        }
        #endregion

        #region UIElement Overrides
        /// <summary>
        /// Déclenche l'événement <see cref="FrameworkElement.SizeChanged"/> à l'aide des informations spécifiées faisant partie de données d'événement définitives. 
        /// </summary>
        /// <param name="sizeInfo">Détails des anciennes et nouvelles tailles sur lesquelles la modification porte.</param>
        protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
        {
            base.OnRenderSizeChanged(sizeInfo);

            _viewport = new SlimDX.Direct3D10.Viewport(0, 0, (int)this.ActualWidth, (int)this.ActualHeight, 0.0f, 1.0f);
            _viewportRect = new Rect(0, 0, this.ActualWidth, this.ActualHeight);

            _renderTargetView = CreateRenderTargetView((int)ActualWidth, (int)ActualHeight, out _sharedTexture);
            _depthStencilView = CreateDepthStencilView((int)ActualWidth, (int)ActualHeight, out _depthTexture);

            BeginRenderingScene();
        }

        /// <summary>
        /// En cas de substitution dans une classe dérivée, participe aux opérations de restitution qui sont menées par le système de disposition.
        /// Les instructions de rendu pour cet élément ne sont pas utilisées directement lorsque cette méthode est appelée, et sont en fait conservées en vue de leur utilisation asynchrone ultérieure pour la disposition et le dessin.
        /// </summary>
        /// <param name="drawingContext">Instructions de dessin pour un élément spécifique.Ce contexte est fourni au système de disposition.</param>
        protected override void OnRender(System.Windows.Media.DrawingContext drawingContext)
        {
            if (object.ReferenceEquals(drawingContext, null))
                throw new ArgumentNullException("drawingContext");

            if (object.ReferenceEquals(_renderTargetView, null) || object.ReferenceEquals(_depthStencilView, null))
                return;

            //Paramétrage de l'Output Merger
            Engine.Device10.OutputMerger.SetTargets(_depthStencilView, _renderTargetView);

            //Paramétrage du Rasterizer
            Engine.Device10.Rasterizer.SetViewports(_viewport);
            
            //Nettoie le buffer de sortie
            Engine.Device10.ClearRenderTargetView(_renderTargetView, _background);
            Engine.Device10.ClearDepthStencilView(_depthStencilView, DepthStencilClearFlags.Depth | DepthStencilClearFlags.Stencil, 1.0f, 0);

            DrawingContext d3d10Context = new DrawingContext()
            {
                WorldMatrix = SlimDX.Matrix.Identity,
                ViewMatrix = Camera.ViewMatrix,
                ProjectionMatrix = Camera.ProjectionMatrix,
                Light = Light,
                CameraPosition = Camera.EyeWorldPosition,
                Time = _timer.ElapsedMilliseconds
            };

            if (Skybox != null) ((IDrawable)Skybox).Draw(d3d10Context);

            foreach (var item in Children)
            {
                item.OnDraw(d3d10Context);
            }
            
            Engine.Device10.Flush();

            drawingContext.DrawImage(_d3d10Image, _viewportRect);

        }
        #endregion
    }
}
